iT邦幫忙

2023 iThome 鐵人賽

DAY 10
0

原簡體中文教程連結: Introduction.《Terraform入門教程》


1.4.6.1. 資源

資源是 Terraform 最重要的組成部分,而本節也是本教學最重要的一節。資源透過 resource 區塊來定義,一個 resource 可以定義一個或多個基礎設施資源對象,例如 VPC、虛擬機,或是 DNS 記錄、Consul 的鍵值對資料等。

1.4.6.1.1. 資源語法

資源透過 resource 塊定義,我們先來講解透過 resource 塊定義單一資源物件的場景。

resource "aws_instance" "web" {
  ami           = "ami-a1b2c3d4"
  instance_type = "t2.micro"
}

跟著 resource 關鍵字的是資源類型,在上面的例子裡就是 aws_instance。後面是資源的 Local Name,例子裡就是 web。Local Name 可以在同一模組內的程式碼裡被用來引用該資源,但類型加 Local Name 的組合在目前模組內必須是唯一的,不同類型的兩個資源 Local Name 可以相同。隨後的花括號內的內容就是塊體,創建資源所用到的各種參數的值就在塊體中定義。範例中我們定義了虛擬機器所使用的鏡像 id 以及虛擬機器的尺寸。

1.4.6.1.2. 資源參數

不同資源定義了不同的可賦值的屬性,官方文件稱之為參數(Argument),有些參數是必填的,有些參數是可選的。使用某項資源前可以透過閱讀相關文件來了解參數清單以及他們的意義、賦值的限制條件。

參數值可以是簡單的字面量,也可以是一個複雜的表達式。

1.4.6.1.3. 資源類型的文檔

每一個 Terraform Provider 都有自己的文檔,用以描述它所支援的資源類型種類,以及每種資源類型所支援的屬性清單。

大部分的公共 Provider 都是透過 Terraform Registry 連帶文件一起發布的。當我們在 Terraform Registry 網站上瀏覽 Provider 的頁面時,我們可以點擊 "Documentation" 連結來瀏覽相關文件。Provider 的文件都是版本化的,我們可以選擇特定版本的 Provider 文件。

要注意的是,Provider 文件曾經是直接託管在 terraform.io 站點上的,也就是 Terraform 核心主站的一部分,有些 Provider 的文檔目前依然託管在那裡,但目前 Terraform Rregistry 才是所有公共 Provider 文檔的主站(唯一的例外是用來讀取其他 Terraform 狀態資料的內建的 terraform provider,它的文檔目前不在 Terraform Registry 上)。

1.4.6.1.4. 資源的行為

一個 resource 區塊聲明了作者想要建立的一個確切的基礎設施對象,並且設定了各項屬性的值。如果我們正在編寫一個新的 Terraform 程式碼文件,那麼程式碼所定義的資源僅僅只在程式碼中存在,並沒有與之對應的實際的基礎設施資源存在。

對一組 Terraform 程式碼執行 terraform apply 可以建立、更新或銷毀實際的基礎設施對象,Terraform 會制定並執行變更計劃,以使得實際的基礎設施符合程式碼的定義。

每當 Terraform 依照一個 resource 塊創建了一個新的基礎設施對象,這個實際的對象的 id 會被保存進 Terraform 狀態中,使得將來 Terraform 可以根據變更計劃對它進行更新或是銷毀操作。如果一個 resource 區塊所描述的資源在狀態檔案中已有記錄,那麼 Terraform 會比對記錄的狀態與程式碼描述的狀態,如果有必要, Terraform 會制定變更計畫以使得資源狀態能夠符合程式碼的描述。

這種行為適用於所有資源而無關其類型。創造、更新、銷毀一個資源的細節會根據資源類型而不同,但是這個行為規則卻是普適的。

1.4.6.1.5. 存取資源輸出屬性

資源不但可以透過參數傳值,成功創建的資源還對外輸出一些透過呼叫 API 才能獲得的唯讀數據,經常包含了一些我們在實際創建一個資源之前無法獲知的數據,比如雲主機的 id 等,官方文檔將之稱為屬性(Attribute)。我們可以在同一模組內的程式碼中引用資源的屬性來建立其他資源或是表達式。在表達式中引用資源屬性的語法是 <RESOURCE TYPE>.<NAME>.<ATTRIBUTE>

要取得一個資源類型輸出的屬性列表,我們可以查閱對應的 Provider 文檔,一般在文檔中會專門記錄資源的輸出屬性列表。

1.4.6.1.5.1. 敏感的資源屬性

在為資源類型定義架構時,Provider 開發著可以將某些屬性標記為 sensitive,在這種情況下,Terraform 會在展示涉及該屬性的計畫時顯示佔位符標記 (sensitive) 而不是實際值。

標記為 sensitive 的 Provider 屬性的行為類似於宣告為 的 sensitive 輸入變量,Terraform 將隱藏計劃中的值,也將隱藏從該值派生出的任何其他敏感值。但是,該行為存在一些限制,如 Terraform 可能會暴露敏感變數。

如果使用資源屬性中的敏感值作為輸出值的一部分,Terraform 將要求將輸出值本身標記為 sensitive,以確認確實打算將其匯出。

Terraform 仍會在狀態中記錄敏感值,因此任何可以存取狀態資料的人都可以以明文形式存取敏感值。

注意:Terraform 從 v0.15 開始將從敏感資源屬性派生的值視為敏感值本身。早期版本的 Terraform 將隱藏敏感資源屬性的直接值,但不會自動隱藏從敏感資源屬性派生的其他值。

1.4.6.1.6. 資源的依賴關係

我們在介紹輸出值的 depends_on 時候已經簡單介紹過了依賴關係。一般來說在 Terraform 程式碼定義的資源之間不會有特定的依賴關係,Terraform 可以並行地對多個無依賴關係的資源執行變更,預設情況下這個並行度是 10。

然而,創建某些資源所需的資訊依賴於另一個資源創建後輸出的屬性,又或者必須在某些資源成功創建後才可以被創建,這時資源之間就存在依賴關係。

大部分資源間的依賴關係可以被 Terraform 自動處理,Terraform 會分析 resource 區塊內的表達式,根據表達式的引用鏈來決定資源之間的引用,進而計算出資源在建立、更新、銷毀時的執行順序。大部分情況下,我們不需要明確指定資源之間的依賴關係。

然而,有時候某些依賴關係是無法從程式碼推導出來的。例如,Terraform 必須要建立一個存取控制權限資源,以及另一個需要該權限才能成功建立的資源。後者的創建依賴於前者的成功創建,然而這種依賴在程式碼中沒有表現為資料引用關聯,在這種情況下,我們需要 depends_on 用來這種依賴關係。

1.4.6.1.7. 元參數

resource 區塊支援幾種元參數聲明,這些元參數可以被聲明在所有類型的 resource 區塊內,它們將會改變資源的行為:

  • depends_on:明確聲明依賴關係
  • count:建立多個資源實例
  • for_each:迭代集合,為集合中每個元素建立一個對應的資源實例
  • provider:指定非預設 Provider 實例
  • lifecycle:自訂資源的生命週期行為
  • provisioner 和 connection:在資源建立後執行一些額外的操作
    下面我們將逐一講解他們的用法。

1.4.6.1.7.1. depends_on

使用 depends_on 可以明確聲明資源之間哪些 Terraform 無法自動推導出的隱含的依賴關係。只有當資源間確實存在依賴關係,但是彼此間又沒有資料引用的場景下才有必要使用 depends_on

使用 depends_on 的例子是這樣的:

resource "aws_iam_role" "example" {
  name = "example"

  # assume_role_policy is omitted for brevity in this example. See the
  # documentation for aws_iam_role for a complete example.
  assume_role_policy = "..."
}

resource "aws_iam_instance_profile" "example" {
  # Because this expression refers to the role, Terraform can infer
  # automatically that the role must be created first.
  role = aws_iam_role.example.name
}

resource "aws_iam_role_policy" "example" {
  name   = "example"
  role   = aws_iam_role.example.name
  policy = jsonencode({
    "Statement" = [{
      # This policy allows software running on the EC2 instance to
      # access the S3 API.
      "Action" = "s3:*",
      "Effect" = "Allow",
    }],
  })
}

resource "aws_instance" "example" {
  ami           = "ami-a1b2c3d4"
  instance_type = "t2.micro"

  # Terraform can infer from this that the instance profile must
  # be created before the EC2 instance.
  iam_instance_profile = aws_iam_instance_profile.example

  # However, if software running in this EC2 instance needs access
  # to the S3 API in order to boot properly, there is also a "hidden"
  # dependency on the aws_iam_role_policy that Terraform cannot
  # automatically infer, so it must be declared explicitly:
  depends_on = [
    aws_iam_role_policy.example,
  ]
}

讓我們來分段解釋一下這個場景,首先我們聲明了一個 AWS IAM 角色,將角色綁定在一個主機實例設定檔上:

resource "aws_iam_role" "example" {
  name = "example"

  # assume_role_policy is omitted for brevity in this example. See the
  # documentation for aws_iam_role for a complete example.
  assume_role_policy = "..."
}

resource "aws_iam_instance_profile" "example" {
  # Because this expression refers to the role, Terraform can infer
  # automatically that the role must be created first.
  role = aws_iam_role.example.name
}

虛擬機器的聲明程式碼中的這個賦值使得 Terraform 能夠判斷虛擬機器依賴主機實例設定檔:

resource "aws_instance" "example" {
  ami           = "ami-a1b2c3d4"
  instance_type = "t2.micro"

  # Terraform can infer from this that the instance profile must
  # be created before the EC2 instance.
  iam_instance_profile = aws_iam_instance_profile.example

至此,Terraform 規劃出的建立順序是 IAM 角色->主機實例設定檔->主機實例。但是我們又為這個 IAM 角色增加了對 S3 儲存服務的完全控制權限:

resource "aws_iam_role_policy" "example" {
  name   = "example"
  role   = aws_iam_role.example.name
  policy = jsonencode({
    "Statement" = [{
      # This policy allows software running on the EC2 instance to
      # access the S3 API.
      "Action" = "s3:*",
      "Effect" = "Allow",
    }],
  })
}

也就是說,虛擬機器實例由於綁定了主機實例配置文件,從而在運行時擁有了一個 IAM 角色,而這個 IAM 角色又被賦予了 S3 的權限。但是虛擬機器實例的聲明程式碼中並沒有引用 S3 權限的任何輸出屬性,這將導致 Terraform 無法理解他們之間存在依賴關係,進而可能會並行地創建兩者,如果虛擬機器實例先創建了出來,內部的程式開始運作時,它所需要的 S3 權限卻還沒創建完成,那就會導致程式執行錯誤。為了確保虛擬機器創建時S3權限一定已經存在,我們可以用 depends_on 明確聲明它們的依賴關係:

# However, if software running in this EC2 instance needs access
  # to the S3 API in order to boot properly, there is also a "hidden"
  # dependency on the aws_iam_role_policy that Terraform cannot
  # automatically infer, so it must be declared explicitly:
  depends_on = [
    aws_iam_role_policy.example,
  ]

depends_on 的賦值必須是包含同一模組內聲明的其他資源名稱的列表,不允許包含其他表達式,例如不允許使用其他資源的輸出屬性,這是因為 Terraform 必須在計算資源間關係之前就能理解列表中的值,為了能夠安全地完成表達式計算,所以限制只能使用資源實例的名稱。

depends_on 只能作為最後的手段使用,如果我們使用 depends_on,我們應該用註解記錄我們使用它的原因,以便今後程式碼的維護者能夠理解隱藏的依賴關係。

1.4.6.1.7.2. count

一般來說,一個 resource 區塊定義了一個對應的實際基礎設施資源物件。但是有時候我們希望創建多個相似的對象,例如創建一組虛擬機器。Terraform 提供了兩種方法來實現這個目標:countfor_each

count 參數可以是任意自然數,Terraform 會建立 count 資源實例,每個實例都對應了一個獨立的基礎設施對象,並且在執行 Terraform 程式碼時,這些對像是被分別建立、更新或銷毀的:

resource "aws_instance" "server" {
  count = 4 # create four similar EC2 instances

  ami           = "ami-a1b2c3d4"
  instance_type = "t2.micro"

  tags = {
    Name = "Server ${count.index}"
  }
}

我們可以在 resource 區塊中的表達式裡使用 count 物件來取得目前的 count 索引號碼。count 物件只有一個屬性:

  • count.index:代表目前物件對應的 count 下標索引(從0開始)

如果一個 resource 區塊定義了 count 參數,那麼 Terraform 會把這種多資源實例物件與沒有 count 參數的單一資源實例物件區分開:

  • 存取單一資源實例物件:<TYPE>.<NAME>(例如:aws_instance.server)
  • 存取多資源實例物件:<TYPE>.<NAME>[<INDEX>](例如:aws_instance.server[0]aws_instance.server[1])

聲明了 countfor_each 的資源必須使用下標索引或鍵來存取。

count 參數可以是任意自然數,然而與 resource 的其他參數不同,count 的值在 Terraform 進行任何遠端資源操作(實際的增刪改查)之前必須是已知的,這也就意味著賦予 count 參數的表達式不可以引用任何其他資源的輸出屬性(例如由其他資源物件建立時傳回的一個唯一的 ID)。count 的表達式中可以引用來自 data 傳回的輸出屬性,只要 data 可以不依賴任何其他 resource 進行查詢。

1.4.6.1.7.3. for_each

for_each 是 Terraform 0.12.6 開始引進的新功能。一個 resource 區塊不允許同時聲明 countfor_eachfor_each 參數可以是一個 map 或是一個 set(string),Terraform 會為集合中每一個元素都創建一個獨立的基礎設施資源對象,和 count 一樣,每一個基礎設施資源對像在執行 Terraform 代碼時都是獨立創建、修改、銷毀的。

使用 map 的範例:

resource "azurerm_resource_group" "rg" {
  for_each = {
    a_group = "eastus"
    another_group = "westus2"
  }
  name     = each.key
  location = each.value
}

使用 set(string) 的範例:

resource "aws_iam_user" "the-accounts" {
  for_each = toset( ["Todd", "James", "Alice", "Dottie"] )
  name     = each.key
}

我們可以在聲明了 for_each 參數的 resource 區塊內使用 each 物件來存取目前的迭代器物件:

  • each.key:map 的鍵,或是 set 中的值
  • each.value:map 的值,或是 set 中的值
    如果 for_each 的值是一個 set,那麼 each.keyeach.value 是相等的。

使用 for_each 時,map 的所有鍵、set 的所有 string 值都必須是已知的,也就是狀態檔中已有記錄的值。所以有時候我們可能需要在執行 terraform apply 時加入 -target 參數,實現逐步創建。另外,for_each 所使用的鍵集合不能夠包含或依賴非純函數,也就是重複執行會傳回不同傳回值的函數,例如 uuidbcrypttimestamp 等。

當一個 resource 宣告了 for_each 時,Terraform 會把這種多資源實例物件與沒有 count 參數的單一資源實例物件區分開:

  • 存取單一資源實例物件:<TYPE>.<NAME>(例如:aws_instance.server)
  • 存取多資源實例物件:<TYPE>.<NAME>[<KEY>](例如:aws_instance.server["ap-northeast-1"]aws_instance.server["ap-northeast-2"])

聲明了 countfor_each 的資源必須使用下標索引或鍵來存取。

由於 Terraform 沒有用以宣告 set 的字面量,所以我們有時需要使用 toset 函數把 list(string) 轉換為set(string):

locals {
  subnet_ids = toset([
    "subnet-abcdef",
    "subnet-012345",
  ])
}

resource "aws_instance" "server" {
  for_each = local.subnet_ids

  ami           = "ami-a1b2c3d4"
  instance_type = "t2.micro"
  subnet_id     = each.key # note: each.key and each.value are the same for a set

  tags = {
    Name = "Server ${each.key}"
  }
}

這裡我們用 toset 把一個 list(string) 轉換成了 set(string),然後賦予 for_each。在轉換過程中,list 中所有重複的元素會被拋棄,只剩下不重複的元素,例如 toset(["b", "a", "b"]) 的結果只有 "a""b" ,而 set 的元素沒有特定順序。

如果我們要把一個輸入變數賦予 for_each,我們可以直接定義變數的型別約束來避免明確呼叫轉換 toset 類型:

variable "subnet_ids" {
  type = set(string)
}

resource "aws_instance" "server" {
  for_each = var.subnet_ids

  # (and the other arguments as above)
}

1.4.6.1.7.4. 在for_each和count之間選擇

如果創建的資源實例彼此之間幾乎完全一致,那麼 count 比較合適。如果彼此之間的參數差異無法直接從 count 的下標派生,那麼使用 for_each 會更加安全。

在 Terraform 引入 for_each 之前,我們經常使用 count.index 搭配 length 函數和 list 來建立多個資源實例:

variable "subnet_ids" {
  type = list(string)
}

resource "aws_instance" "server" {
  # Create one instance for each subnet
  count = length(var.subnet_ids)

  ami           = "ami-a1b2c3d4"
  instance_type = "t2.micro"
  subnet_id     = var.subnet_ids[count.index]

  tags = {
    Name = "Server ${count.index}"
  }
}

這種實作方法是脆弱的,因為資源仍然是以他們的下標而不是實際的 string 值來區分的。如果我們從 subnet_ids 清單的中間移除了一個元素,那麼從該位置起後續所有的 aws_instance 都會發現它們的 subnet_id 發生了變化,結果就是所有後續的 aws_instance 都需要更新。這種場景下如果使用 for_each 就更為妥當,如果使用 for_each,那麼只有被移除的 subnet_id 對應的 aws_instance 會被銷毀。

1.4.6.1.7.5. provider

關於 provider 的定義我們在前面介紹 Provider 的章節已經提到過了,如果我們聲明了同一類型 Provider 的多個實例,那麼我們在創建資源時可以透過指定 provider 參數選擇要使用的 Provider 實例。如果沒有指定 provider 參數,那麼 Terraform 預設使用資源類型名稱中第一個單字所對應的 Provider 實例,例如 google_compute_instance 的預設 Provider 實例就是 googleaws_instance 的預設 Provider 就是 aws

指定 provider 參數的例子:

# default configuration
provider "google" {
  region = "us-central1"
}

# alternate configuration, whose alias is "europe"
provider "google" {
  alias  = "europe"
  region = "europe-west1"
}

resource "google_compute_instance" "example" {
  # This "provider" meta-argument selects the google provider
  # configuration whose alias is "europe", rather than the
  # default configuration.
  provider = google.europe

  # ...
}

provider 參數期待的賦值是 <PROVIDER> 或是 <PROVIDER>.<ALIAS>,不需要雙引號。因為當 Terraform 開始計算依賴路徑圖時,provider 關係必須是已知的,所以除了這兩種以外的表達式是不被接受的。

1.4.6.1.7.6. lifecycle

通常一個資源物件的生命週期在前面「資源的行為」一節中已經描述了,但是我們可以用區塊來 lifecycle 定一個不一樣的行為方式,例如:

resource "azurerm_resource_group" "example" {
  # ...

  lifecycle {
    create_before_destroy = true
  }
}

lifecycle 區塊和它的內容都屬於元參數,可以被宣告於任意類型的資源區塊內部。Terraform 支援如下幾種 lifecycle

  • create_before_destroy(bool):預設情況下,當 Terraform 需要修改一個因為服務端API限製而無法直接升級的資源時,Terraform 會刪除現有資源對象,然後用新的組態參數建立新的資源對象取代之。create_before_destroy 參數可以修改這個行為,使得 Terraform 先建立新對象,只有在新對象成功建立並取代舊對象後再銷毀舊對象。這並不是預設的行為,因為許多基礎設施資源需要有一個唯一的名字或是別的什麼識別屬性,在新舊物件並存時也要符合這種約束。有些資源類型有特別的參數可以為每個物件名稱添加一個隨機的前綴以防止衝突。Terraform 不能預設採用這種行為,所以在使用 create_before_destroy 前你必須了解每一種資源類型在這方面的限制。
  • prevent_destroy(bool):這個參數是一個保險措施,只要它被設定 true 為時,Terraform 會拒絕執行任何可能會銷毀該基礎設施資源的變更計畫。這個參數可以預防意外刪除關鍵資源,例如錯誤地執行了 terraform destroy,或是意外修改了資源的某個參數,導致 Terraform 決定刪除並重建新的資源實例。在 resource 區塊內聲明了 prevent_destroy = true 會導致無法執行 terraform destroy,所以對它的使用要節制。需要注意的是,該措施無法防止我們刪除 resource 區塊後 Terraform 刪除相關資源,因為對應的 prevent_destroy = true 聲明也被一併刪除了。
  • ignore_changes(list(string)):預設情況下,Terraform 偵測到程式碼描述的配置與真實基礎設施對象之間有任何差異時都會計算一個變更計畫來更新基礎設施對象,使其符合程式碼描述的狀態。在一些非常罕見的場景下,實際的基礎設施物件會被 Terraform 以外的流程所修改,這就會使得 Terraform 不停地嘗試修改基礎設施物件以彌合和程式碼之間的差異。在這種情況下,我們可以透過設定 ignore_changes 來指示 Terraform 忽略某些屬性的變更。ignore_changes 的值定義了一組在創建時需要按照程式碼定義的值來創建,但在更新時不需要考慮值的變化的屬性名,例如:
resource "aws_instance" "example" {
  # ...

  lifecycle {
    ignore_changes = [
      # Ignore changes to tags, e.g. because a management agent
      # updates these based on some ruleset managed elsewhere.
      tags,
    ]
  }
}
  • 你也可以忽略 map 中特定的元素,例如 tags["Name"],但要注意的是,如果你是想忽略 map 中特定元素的變更,那麼你必須先確保 map 中含有這個元素。如果一開始 map 中並沒有這個鍵,而後外部系統加入了這個鍵,那麼 Terraform 還是會把它當成一次變更來處理。比較好的方法是你在程式碼中先為這個鍵建立一個佔位元素來確保這個鍵已經存在,這樣在外部系統修改了鍵對應的值以後 Terraform 會忽略這個變更。
resource "aws_instance" "example" {
  # ...

  tags = {
    # Initial value for Name is overridden by our automatic scheduled
    # re-tagging process; changes to this are ignored by ignore_changes
    # below.
    Name = "placeholder"
  }

  lifecycle {
    ignore_changes = [
      tags["Name"],
    ]
  }
}
  • 除了使用一個 list(string),也可以使用關鍵字 "all",這時 Terraform 會忽略資源一切屬性的變更,這樣Terraform 只會創建或銷毀一個對象,但絕不會嘗試更新一個對象。你只能在 ignore_changes 裡忽略所屬的 resource 的屬性,ignore_changes 不可以賦予它本身或是其他任何元參數。
  • replace_triggered_by(包含資源參考的清單):強制 Terraform 在引用的資源或是資源屬性發生變更時替換宣告該區塊的父資源,值為一個包含了託管資源、實例或是實例屬性參考表達式的清單。當宣告該區塊的資源宣告了 count 或是 for_each 時,我們可以在表達式中使用 count.index 或是 each.key 來指定引用實例的序號。

replace_triggered_by 可以在以下幾種場景中使用:

  • 如果表達式指向多實例的資源聲明(例如聲明了 count 或是 for_each 的資源),那麼這組資源中任意實例發生變更或被替換時都會引發聲明 replace_triggered_by 的資源被替換
  • 如果表達式指向單一資源實例,那麼該實例發生變更或被替換時將引發聲明 replace_triggered_by 的資源被替換
  • 如果表達式指向單一資源實例的單一屬性,那麼該屬性值的任何變更都會引發宣告 replace_triggered_by 的資源被替換

我們在 replace_triggered_by 中只能引用託管資源。這允許我們在不引發強制替換的前提下修改這些表達式。

resource "aws_appautoscaling_target" "ecs_target" {
  # ...
  lifecycle {
    replace_triggered_by = [
      # Replace `aws_appautoscaling_target` each time this instance of 
      # the `aws_ecs_service` is replaced.
      aws_ecs_service.svc.id
    ]
  }
}

lifecycle 配置影響了 Terraform 如何建構並遍歷相依圖。作為結果,lifecycle 內賦值僅支援字面量,因為它的計算過程發生在 Terraform 計算的極早期。這就是說,例如 prevent_destroycreate_before_destroy 的值只能是 true 或者 falseignore_changesreplace_triggered_by 的列表內只能是硬編碼的屬性名。

1.4.6.1.7.7. Precondition 與Postcondition

請注意,Precondition 與 Postcondition 是從 Terraform v1.2.0 開始被引入的功能。

lifecycle 區塊中聲明 preconditionpostcondition 區塊可以為資源、資料來源以及輸出值建立自訂的驗證規則。

Terraform 在計算物件之前會先檢查該物件關聯的 precondition,並且在物件計算完成後執行 postcondition 檢查。Terraform 會盡可能早地執行自訂檢查,但如果表達式中包含了只有在 apply 階段才能知曉的值,那麼該檢查也將被推遲執行。

每一個 preconditionpostcondition 區塊都需要一個 condition 參數。此參數是一個表達式,在滿足條件時返回 true,否則返回 false。此表達式可以引用同一模組內的任意其他對象,只要這種引用不會產生環依賴。在 postcondition 表達式中也可以使用 self 物件參考聲明 postcondition 的資源實例的屬性。

如果 condition 表達式計算結果為 false,Terraform 會產生錯誤訊息,包含了 error_message 表達式的內容。如果我們聲明了多個條 preconditionpostcondition,Terraform 會傳回所有失敗條件對應的錯誤訊息。

下面的例子示範了透過 postcondition 偵測呼叫者是否不小心傳入了錯誤的 AMI 參數:

data "aws_ami" "example" {
  id = var.aws_ami_id

  lifecycle {
    # The AMI ID must refer to an existing AMI that has the tag "nomad-server".
    postcondition {
      condition     = self.tags["Component"] == "nomad-server"
      error_message = "tags[\"Component\"] must be \"nomad-server\"."
    }
  }
}

resourcedata 區塊中的 lifecycle 區塊可以同時包含 preconditionpostcondition 區塊。

  • Terraform 會在計算完 countfor_each 元參數後執行 precondition 區塊。這使得 Terraform 可以對每一個實例獨立進行檢查,並允許在表達式中使用 each.keycount.index 等。Terraform 也會在計算資源的參數表達式之前執行 precondition 檢查。precondition 可以用來防止參數表達式計算中的錯誤被激發。
  • Terraform 在計算和執行對一個託管資源的變更之後執行 postcondition 檢查,或在完成資料來源讀取後執行它關聯的 postcondition 檢查。postcondition 失敗會阻止其他依賴此失敗資源的其他資源的變更。

在大多數情況下,我們不建議在同一設定檔中同時包含表示同一個物件的 data 區塊和 resource 區塊。這樣做會使得 Terraform 無法理解 data 區塊的結果會被 resource 區塊的變更所影響。然而,當我們需要檢查一個 resource 區塊的結果,而恰巧該結果又沒有被資源直接輸出時,我們可以使用 data 區塊並在區塊中直接使用 postcondition 來檢查該物件。這等於是告訴 Terraform 該 data 區塊是用來檢查其他什麼地方定義的物件的,從而允許 Terrform 以正確的順序執行操作。

1.4.6.1.7.8. provisioner 和connection

某些基礎設施物件需要在創建後執行特定的操作才能正式工作。比如說,主機實例必須在上傳了設定或是由設定管理工具初始化之後才能正常運作。

像這樣創建後執行的操作可以使用預置器(Provisioner)。預置器是由 Terraform 提供的另一組插件,每種預置器可以在資源物件建立後執行不同類型的操作。

使用預置器需要節制,因為他們採取的操作並非 Terraform 聲明式的風格,所以 Terraform 無法對他們執行的變更進行建模和保存。

預置器也可以聲明為資源銷毀前執行,但會有一些限制。

作為元參數,provisionerconnection 可以聲明在任意類型的 resource 區塊內。

舉個例子:

resource "aws_instance" "web" {
  # ...

  provisioner "file" {
  source       = "conf/myapp.conf"
  destination  = "/etc/myapp.conf"

    connection {
      type     = "ssh"
      user     = "root"
      password = var.root_password
      host     = self.public_ip
    }
  }
}

我們在 aws_instance 中定義了類型為的 file 預置器,該預置器可以本機檔案或資料夾拷貝到目標機器的指定路徑下。我們在預置器內部定義了 connection 區塊,類型是 ssh。我們對 connectionhost 賦值 self.public_ip,在這裡 self 代表預置器所在的母塊,也就是 aws_instance.web,所以 self.public_ip 代表著 aws_instance.web.public_ip,也就是創建出來的主機的公網 ip。

file 類型預置器支援 sshwinrm 兩種類型的 connection

預置器根據運行的時機分為兩種類型,創建時預置器以及銷毀時預置器。

1.4.6.1.8. 建立時預置器

預設情況下,資源物件被建立時會執行預置器,在物件更新、銷毀時則不會運作。預置器的預設行為時為了引導一個系統。

如果建立時預置器失敗了,那麼資源物件會被標記污點(我們將在介紹 terraform taint 指令時詳細介紹)。一個被標記污點的資源在下次執行 terraform apply 指令時會被銷毀並重建。Terrform的這種設計是因為當預設器運作失敗時標誌著資源處於半就緒的狀態。由於 Terraform 無法衡量預置器的行為,所以唯一能完全確保資源正確初始化的方式就是刪除重建。

我們可以透過設定 on_failure 參數來改變這種行為。

1.4.6.1.9. 銷毀時預置器

如果我們設定預置器的 when 參數為 destroy,那麼預置器會在資源被銷毀時執行:

resource "aws_instance" "web" {
  # ...

  provisioner "local-exec" {
    when    = destroy
    command = "echo 'Destroy-time provisioner'"
  }
}

銷毀時預置器在資源實際銷毀前運作。如果運行失敗,Terraform 會報錯,並在下次執行 terraform apply 操作時重新執行預置器。在這種情況下,需要仔細注意銷毀時預置器以使之能夠安全地重複執行。

銷毀時預置器只有在存在於程式碼中的情況下才會在銷毀時執行。如果一個 resource 區塊連帶內部的銷毀時預置器區塊一起被從程式碼中刪除,那麼被刪除的預置器在資源被銷毀時就不會被執行。要解決這個問題,我們需要使用多個步驟來繞過這個限制:

  • 修改資源聲明程式碼,新增 count = 0 參數
  • 執行 terraform apply,運行刪除時預置器,然後刪除資源實例
  • 刪除 resource 區塊
  • 重新執行 terraform apply,此時應該不會有任何變更需要執行

這個限制在未來將會得到解決,但目前來說我們必須節制使用銷毀時預置器。

1.4.6.1.10. 預置器失敗行為

預設情況下,預置器運作失敗會導致 terraform apply 執行失敗。可以透過設定 on_failure 參數來改變這一行為。可以設定的值為:

  • continue:忽略錯誤,繼續執行創建或銷毀
  • fail:報錯並終止執行變更(這是預設行為)。如果這是一個建立時預置器,則在對應資源物件上標記污點
    範例:
resource "aws_instance" "web" {
  # ...

  provisioner "local-exec" {
    command    = "echo The server's IP address is ${self.private_ip}"
    on_failure = continue
  }
}

1.4.6.1.11. 本地資源

雖然大部分資源類型都對應的是透過遠端基礎設施 API 控制的一個資源對象,但也有一些資源對像他們只存在於 Terraform 進程自身內部,用來計算產生某些結果,並將這些結果保存在狀態中以備日後使用。

比如說,我們可以用 tls_private_key 生成公私鑰,用 tls_self_signed_cert 生成自簽名證書,或是用 random_id 生成隨機 id。雖然不像其他「真實」基礎設施物件般重要,但這些本地資源也可以成為連接其他資源有用的黏合劑。

本地資源的行為與其他類型資源是一致的,但是他們的結果資料僅存在於 Terraform 狀態檔案中。「銷毀」這種資源只是將結果資料從狀態中刪除。

1.4.6.1.12. 操作超時設定

有些資源類型提供了特殊的 timeouts 內嵌塊參數,它允許我們配置我們允許操作持續多長時間,逾時將被認定為失敗。比如說,aws_db_instance 資源允許我們分別為 createupdatedelete 操作設定超時時間。

超時完全由資源對應的 Provider 來處理,但支援超時設定的 Provider 一般都遵循相同的傳統,那就是由一個名為 timeouts 的嵌入塊參數定義超時設置,timeouts 內可以分別設置不同操作的超時時間。超時時間由 string 描述,例如 "60m" 代表 60 分鐘,"10s" 代表 10 秒,"2h" 代表 2 小時。

resource "aws_db_instance" "example" {
  # ...

  timeouts {
    create = "60m"
    delete = "2h"
  }
}

可設定逾時的操作類別由每種支援逾時設定的資源類型自行決定。大部分資源類型不支援設定超時。使用超時前請先查閱相關文件。


原簡體中文教程連結: Introduction.《Terraform入門教程》


上一篇
Day9-【入門教程】Terraform代碼的書寫—輸出值及局部值
下一篇
Day11-【入門教程】資料來源
系列文
Terraform 繁體中文25
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言